Uber 新架构 RIBs 的前世今生

Uber 新架构 RIBs 的前世今生

为什么 Uber 要重构移动端

Uber 基于一个简单的概念:一键出行。 从最初优享到现在提供的一系列产品,每天在数百个城市协调数百万次乘车。 为了应对和支持2017年及以后的发展,我们迫切的需要重新设计我们的移动端架构。

但从哪里开始? 我们决定重新开始。于是我们决定完全重构并重新设计我们的乘客端。 由于不用被之前的设计和代码限制,在重构上我们有很大的发挥空间。结果就是你今天看到的这个时尚的新应用, 它在iOS和Android上实现了新的移动架构。接下来的文章将介绍我们的新移动端架构 Riblets,让你了解为什么我们需要创建这种新架构模式,以及它如何帮助我们达成目标。

目标

虽然共享出行仍然是 Uber 背后的驱动理念,但我们的产品已发展成为功能复杂的APP,我们原有的移动架构无法与之匹配。 随着乘客端App的新功能扩展,工程挑战和技术债务不断累积。增加了诸如拼车预约乘车 和促销车辆视图等功能,导致工程的复杂性逐步升高。 我们的行程模块变得越来越大,难以测试。 加入小变化有可能影响到应用程序的其他部分,使得功能尝试增加额外调试任务,从而抑制了我们快速迭代和功能实验。 为了给所有 Uber 用户的高质量体验,我们需要一种方法,重新找回起点的简单,同时考虑今天的处境和未来的目标。

对于乘客和 Uber 工程师来说,新的应用程序必须简单。 为了适用于不同的群体,我们的两个主要目标是:持续增加有效的核心用户体验,并且允许在系列产品需求序列中做大胆实验。

可靠性

从工程方面来说,我们正在努力使 Uber 的行程主流程的可靠性达到 99.99%。 实现99.99%的可靠性意味着我们每年只能有一个累计小时的停机时间,一周的停机时间为一分钟,每10,000次运行只有一次失败。

为了实现这一目标,新架构定义并实现了核心和可选代码的框架。 核心代码包括注册,获取,完成或取消行程所需的一切代码。 对核心代码的更改和添加需要经过严格的审核流程。 可选代码可以降低审查力度,可以在不停止核心业务的情况下关闭。 这种代码隔离机制使我们能够尝试新功能,并在异常情况下自动关闭它们,而不会干扰乘车体验。

规划

我们需要一个平台,一百个不同的项目团队和数千名工程师可以快速构建高质量的功能,并在乘客端上进行创新,而不会影响核心用户体验。 因此,我们提供了新的移动端架构,具有跨平台兼容性,确保iOS和Android工程师都可以在统一的基础上工作。

从历史上看,在 iOS 和 Android 上发布最好的应用程序涉及不同的架构、库设计和分析方法。 但是,新架构致力于在两个平台上使用相同的最佳模式和实践。 这给了我们学习两个平台的机会。 由于一个平台的经验教训可以预先解决另一个平台上的问题,从而避免了同样的错误在两个平台重复出现。 因此,iOS 和 Android 工程师可以更轻松地进行协作,并且可以并行处理新功能。

虽然在某些情况下,平台之间可以也应该是不同的(例如 UI 实现),但是 iOS 和 Android 移动平台都是从一致性出发。平台共享:

  • 核心架构
  • 类名
  • 业务逻辑单元之间的继承关系
  • 业务逻辑如何划分
  • 插件点 (名字, 存在,结构等)
  • 响应式编程链
  • 统一平台组件

为了实现平台之间的这种通用蓝图,我们的新移动架构需要清晰的组织和分离业务逻辑,视图逻辑,数据流和路由。这种架构有助于降低复杂性,简化可测试性,从而提高工程效率和用户可靠性。 我们在其他架构模式上进行了创新以实现此目标。

从 MVC 到 Riblets

考虑到我们的两个目标,我们调查了旧架构可以改进的地方,并研究了可行的方案。Uber 旧的代码遵循[MVC 模式](https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html)。我们调查了其他模式,特别是[VIPER](https://mutualmobile.com/posts/meet-viper-fast-agile-non-lethal-ios-architecture-framework),我们最终用它来创建 Riblets。Riblets 的核心创新是业务逻辑驱动,而不是视图逻辑驱动。 如果您不熟悉 MVC 和 VIPER,请阅读一些[关于现代 iOS 架构模式的文章](https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.ba5863nnx),然后回过头来看看在 Uber 采用它们的利弊。

MVC (Model-View-Controller)

乘客端应是在大约四年前由少数几个工程师创建的。 虽然 MVC 模式在当时是有意义的,但随着程序的规模越来越大,也就越来越难以管理。 随着业务的增长和团队的扩大, MVC 的弊端越发明显。具体来说,有两大问题:

首先,成熟的 MVC 架构经常面临重量级视图控制器的困境。例如,RequestViewController 刚开始有 300 行代码,由于处理了太多的功能(业务逻辑,数据操作,数据验证,网络逻辑,路由逻辑等),现在超过 3,000 行。它变得难以阅读和维护。

其次,MVC 架构的更新过程是不易维护和测试的。我们进行了大量实验,为用户推出了新功能。 这些实验归结为 if-else 语句。 每当将 if-else 语句构建在一个具有许多功能函数的类上时,导致几乎无法推理,更不用说测试了。 此外,由于像RequestViewController 和 TripViewController 代码巨大并且快速增长,因此对应用程序进行更新变得更加空难。 想象一下,进行更改并测试嵌套 if-else 实验的每种可能组合将是多么的困难。由于我们需要实验来继续添加新功能并增加 Uber 的业务,因此这种架构不具备可扩展性。

VIPER

在考虑 MVC 的替代方案时,我们受到 VIPER 架构的启发。适用于 iOS 应用程序的简洁架构。VIPER 为 MVC 提供了一些关键优化。首先,它提供了更多的抽象。Presenter 桥接视图逻辑和业务逻辑。Interactor 处理纯粹的数据操作和数据验证,包括向服务层发起调用,例如登录或者发单。最后,Router 启动跳转,例如将用户从首页带到确认页。其次,使用 VIPER 方法,Presenter 和 Interactor 是普通对象,因此我们可以进行简单的单元测试。

但我们也发现了 VIPER 的一些缺点。它是 iOS 独有架构,意味着我们必须为 Android 做出权衡。由于整个应用程序被固定在视图树上,也就意味着状态由视图驱动。 Interactor 必须通过 Presenter 操作应用程序的业务逻辑,因此需要暴露业务逻辑给 Presenter。至此,通过紧密耦合的视图树和业务树,很难实现仅包含业务逻辑或仅包含视图逻辑的业务节点,无法达到解藕的目的。

虽然 VIPER 对使用的 MVC 模式进行了重大改进,但它并没有完全满足,清晰的模块化定义,和高可扩展性。所以我们在兼顾 VIPER 优势,同时规避其架构模式缺点的基础上,实现了我们自己的架构: Riblets。

Riblets: Uber 乘客端架构

在我们的新架构模式中,业务逻辑被分解为小的,可独立测试的单元,每个单元目的明确,遵循单一责任原则。 我们使用 Riblets 作为这些模块化部件,整个应用程序结构为 Riblets 树。

Riblets 组件

通过 Riblets,我们将职责分配给六个不同的组件,进一步抽象业务和视图逻辑:

Riblets 与 VIPER 和 MVC 的区别是什么?路由由业务逻辑而非视图逻辑引导。这意味着应用程序由信息流和决策流驱动,而不是 Presenter。在Uber,并非每个业务逻辑都与用户看到的视图相关。不是将业务逻辑集中到 MVC 中的 ViewController 或通过 VIPER 中的 Presenter 操作应用程序状态。我们可以为每个业务逻辑提供不同的 Riblets,这些 Ribltes 可以组合出不同意义的逻辑分组。 Riblet 模式被设计为​​跨平台的,达到统一 Android 和 iOS 架构的目的。

每个 Riblet 由 Router,Interactor 和 Builder 及其 Component 和可选的 Presenters 和 Views 组成。Router 和 Interactor 处理业务逻辑,而 Presenter 和 View 处理视图逻辑。

让我们使用车型切换 Riblet 作为示例,确定每个 Riblet 单元负责的内容。

新乘客端APP,车型切换功能。

Builder

Builder 实例化所有主要 Riblet 单元并定义依赖关系。 在车型切换 Riblet 中,此单元定义城市流(特定城市的数据流)依赖关系。

Component

Component 获取并实例化 Riblet 的依赖项。 这包括服务,数据流以及其他不是主要 Riblet 单元的内容。 车型切换组件获取并实例化城市流依赖关系,将其与对应的网络事件进行关联,并将其注入到 Interactor。

Routers

Routers 通过添加和删除 子Riblets 形成应用程序树,同时驱动组件内 Interactor 的生命周期。 这些决定由外部 Interactor 传递。路由器包含两个业务逻辑:

  1. 添加和删除组件
  2. 子组件间状态切换

车型切换 Riblet 没有任何子 Riblets。 其父 Riblet 的 Router, 确认 Riblet 负责添加车型切换的 Router 并将其 Views 添加到 View 层次结构中。 然后,一旦选择了车型,车型切换 Router 将停用其 Interactor。

Interactors

Interactors 执行业务逻辑:

  • 调用服务来启动操作,比如请求搭车
  • 调用服务来获取数据
  • 决定要转换到下一个的状态。 例如,如果根 Interactor 监听到用户的身份验证令牌过期,它会向其 Router 发送切换到 “欢迎” 状态的请求。

车型切换 Interactor 包含城市流数据,包括该城市服务的车型,定价信息,预估行程时间和车辆视图。 它将此信息传递给 Presenter。 如果用户从拼车切换到优享,则 Interactor 会从 Presenter 接收此信息。 然后它会收集相关数据传给 View,这样它就可以显示 uberX 车辆和预估行程时间。 简而言之,Interactor 执行随后 View 中显示的所有业务逻辑。

View (Controller)

视图构建和更新UI,包括实例化和布局 UI 组件,处理用户交互,UI 组件数据填充和动画。 车型切换 Riblet 的 View 显示它从 Presenter 接收的数据(车型选项,定价,ETA,地图上的车辆视图)并反馈用户操作(即车型切换)。

Presenter

Presenters 管理 Interactors 和 Views 之间的通信。 从 Interactors 到 Views,Presenter 将业务模型转换为 View 可以显示的模型。 对于车型切换,这包括定价数据和车辆视图。 从 Views 到 Interactors,Presenters 将用户交互事件(例如,点击按钮选择车型)转换为 Interactors 中的相应操作。

整合

Riblets 只有一个 Router 和 Interactor,但可以有多个 View 部分。仅处理业务逻辑且没有用户界面元素的 Riblet 没有视图部分。 因此,Riblets 可以是单视图(一个 Presenter 和一个 View),多视图(一个 Presenter 和多个 Views,或多个 Presenter 和 Views),或者是无视图(没有 Presenter 和 View)。 这允许业务逻辑树的结构和深度与视图树不同,视图树将具有更平坦的层次结构。 这有助于简化页面切换。

例如,乘车 Riblet 是一个无视图的 Riblet,用于检查用户是否有有效的行程。如果已经开始行程,它添加行程 Riblet,将行程显示在地图上。如果没有,它将添加请求 Riblet,请求 Riblet 将在屏幕显示,允许用户请求行程。像乘车 Riblet 这样没有视图逻辑的 Riblet,通过分解业务逻辑驱动应用程序,在支持这种新体系结构的模块化方面,发挥了重要作用。

Riblets 构建应用程序

Riblets 组成了应用程序树,并且经常需要进行通信以便更新信息或将用户带到下一阶段。 在我们讨论他们如何通信之前,让我们首先了解数据在一个 Riblet 中是如何流动的。

数据流

Interactors 拥有状态的作用范围和业务逻辑。该单元进行服务调用获取数据。 在新架构中,数据是单方向流动的。 它从 Service 到 Model Stream,然后从 Model Stream 到 Interactor。 来自服务器的交互,调度和推送通知可以要求 Service 对 Model Stream 进行更改。Model Stream 生成不可变模型。 这强制要求 Interactors 类必须使用服务层来更改应用程序的状态。

示例流程:

  • 从后端服务到视图: 服务调用(如状态)从后端获取数据。 将数据放置在不可变 Model Stream 上。 Interactor 监听新数通知并将其传递给 Presenter。 Presenter 格式化数据并将其发送给 View。

  • 从视图到后端: 用户点击按钮(如登录),然后 View 将交互传递给 Presenter。 Presenter 在 Interactor 上调用登录方法,该方法调用 Service 进行登录。 返回的令牌由 Service 在数据流上发布。 Interactor 监听数据流,收到通知后 Interactor 切换 Riblet 到首页 Riblet。

Riblets 间通信

当 Interactor 做出业务逻辑决策时,它可能需要通知另一个 Riblet(例如,完成)并发送数据。为实现此目的,做出业务逻辑决策的 Interactor 调用另一个 Riblet 的 Interactor 。

通常,如果通信是 Riblet 树上,从子 Riblet 传递到父 Riblet 的 Interactor,则该接口被定义为侦听器。侦听器几乎总是由父 Riblet 的 Interactor 实现。如果通信向下传递给子 Riblet,则应将接口定义为代理,并由子 Riblet 的 Interactor 实现。代理仅用于 Riblet 单元之间的同步通信,例如父 Interactor 与子 Interactor 之间的同步。

特别是对于向下通信,作为代理的替代方法, 父 Riblet 可以选择将可观察的数据流暴露给子 Riblet 的 Interactor。然后,父 Riblet 的 Interactor 可以通过此流将数据发送到子 Riblet 的 Interactor。在大多数用于发送数据的向下通信中,这应该是首选的通信方法。

例如,车型切换 Interactor 确定已选择车型时,它会调用其侦听器以传递所选的车辆视图 ID。侦听器由确认 Interactor实现。然后,确认 Interactor 存储车辆视图 ID,以便可以在服务请求中发送,调用其 Router 分离车型切换 Riblet。

通过以上方式构建 Riblets 内部和 Riblets 之间的数据流通信,我们能够确保在正确的页面正确的时间出现正确的数据。因为 Riblets 基于业务逻辑形成应用程序树,所以我们可以通过业务逻辑(而不是视图逻辑)来路由通信。这对我们的业务意义重大,并最终有助于代码隔离,防止应用程序开发变得过于复杂。

回到起点

当我们重新开始乘客端时,希望提高乘客体验的可靠性和为未来的应用程序开发建立标准规范。创建新架构对于实现这两个目标至关重要。

如何提高乘车体验的可靠性 ?

Riblets 有明确的职责划分,因此测试更加简单。每个 Riblet 都是可独立测试的。通过更充分的测试,当推出更新时,我们可以对应用的可靠性更有信心。由于每个 Riblet 只负责一个任务,因此很容易将 Riblet 及其依赖项分离到核心代码和可选代码中。通过对核心代码进行更严格的审查,我们可以对核心流程的可用性更有信心。

我们提供了核心流程全局回滚到可用状态的能力。所有可选代码都具备开关能力,如果部分功能有问题,可以将其关闭。在最糟糕的情况下,我们可以关闭全部可选代码,保留默认的核心流程。由于我们在核心代码上有超高的标准,可以确保我们的核心流程始终有效。

如何为开发建立标准规范 ?

Riblets 帮助我们尽可能缩小和分离功能。清晰的分离业务和视图逻辑,将有助于防止我们的代码库变得过于复杂并使其易于工作。由于新架构与平台无关,因此 iOS 和 Android 工程师可以轻松了解对方如何开发,从一方的错误中吸取教训,并共同推动 Uber 向前发展。由于 Riblets 帮助我们将可选代码与核心代码分开,因此实验将不太容易对核心体验产生附带影响。我们将能够在 Riblet 架构中将新功能作为插件进行尝试,而不必担心它们可能会意外地将 uberX 和 uberPOOL 体验置于bug 的风险之中。

由于 Riblets 加强了抽象和责任分离,并且有明确的数据流和通信路径,因此持续开发变得很容易。这种架构将在未来几年内为我们服务。

星辰大海

我们的新架构使我们为未来的发展做好了准备。最新的重构意味着完全重做乘客端的代码,重新实现以前存在的内容,执行用户研究,案例研究,A/B 测试以及编写新功能。最重要的是,我们希望进行全球推广,以便更快地将新应用程序交付给用户,因此我们从设计,功能,本地化,设备和测试角度考虑了全球变化。 虽然已经投放市场,但我们新架构下的工作才刚刚开始。

---------Thanks for your attention---------
0%